Lambdaオーソライザーのトークンベースとリクエストパラメータベースの挙動を比べて、どちらを選択するべきか考えてみた
いわさです。
Amazon API GatewayにはHTTP APIとREST APIがあります。
それらの2つの違いについては以下が参考になります。
さらに、API Gatewayにはオーソライザー機能があります。
リクエストに対する認可処理を行うことが出来ます。
REST APIでLambdaオーソライザーを構成する場合、トークンベースとリクエストパラメータベースの2つの選択肢が出てきます。
本日はこの2つの挙動の違いと、どういうときにどちらを選択すれば良いのかをまとめました。
共通の下準備と前提
前提条件のようなものも含みますが、この記事でやること / やらないことを最初に書いておきます。
2つのREST APIにリソースと2つ作る
トークンベース用とリクエストパラメータベース用にそれぞれリソースとPOSTメソッドを用意します。
そして、メソッドリクエストにそれぞれのオーソライザーを設定して動作確認します。
検証を通してAPIのデプロイを忘れるシーンが何度もありました。
Lambdaだとデプロイ忘れ気づきやすいのですけどね、気をつけねば。
なお、今回はリクエスト送信は共通してPostmanを使用します。
Lambda関数は共通のものを使う
オーソライザーの設定値以外は共通させます。
なので、トークンベースとリクエストパラメータベースでLambda関数も同じものを使用します。
ただし、オーソライザーとバックエンドのLambda関数は別で用意しました。
オーソライザー用関数は以下のようにログ出力したあと、固定で認証結果を返しています。
import json def lambda_handler(event, context): print(json.dumps(event)) return { 'principalId': 'abc123', 'policyDocument': { 'Version': '2012-10-17', 'Statement': [{ 'Action': 'execute-api:Invoke', 'Effect': 'Allow', 'Resource': event['methodArn'] }] }, 'context': { 'hoge-string': 'hoge', 'hoge-num': 111, 'hoge-bool': True } }
REST APIでのLambdaオーソライザーで返却する必要があるのは、PrincipalId
とPolicyDocument
です。
PrincipalId
は一般的にアプリケーション側で判断するリクエスト元の識別子で、PolicyDocument
のEffect
値で許可するか拒否するか、API Gatewayの挙動が変わります。
import json def lambda_handler(event, context): print(json.dumps(event)) return { 'principalId': 'abc123', 'policyDocument': { 'Version': '2012-10-17', 'Statement': [{ 'Action': 'execute-api:Invoke', 'Effect': 'Deny', 'Resource': event['methodArn'] }] }, 'context': { 'hoge-string': 'hoge', 'hoge-num': 111, 'hoge-bool': True } }
このようにDeny
にするとオーソライザーの評価結果からAPI Gatewayが403ステータスをレスポンスします。
以下はバックエンドの関数です。
送信されたオブジェクトをログ出力しているのみです。
import json def lambda_handler(event, context): print(json.dumps(event)) return { 'statusCode': 200, 'body': json.dumps('Hello from Lambda!') }
HTTP APIは検証しない
今回、HTTP APIは検証しません。REST APIのみです。
HTTP APIのLambda関数の場合は、ペイロード形式バージョンによって返却する形式も異なりますのでご注意ください。
詳細は以下を参照してください。
キャッシュは有効化させない
オーソライザーにはキャッシュ機能がありますが、今回はこのキャッシュは有効化させない前提で考えます。
このキャッシュについては少し挙動の確認が必要なので、あらためて記事にしたいと考えています。
また、イベントペイロードの種類によってキャッシュの動きが少し違います。そのため選定時の判断に関わる可能性もあるので、今回は無効化を前提に調査をしました。
トークンベース
以下はオーソライザー未設定状態の実行結果です。
ここにトークンベースオーソライザーを設定し、挙動を確認していきたいと思います。
オーソライザーの設定
「トークンのソース」にAuthorization
を指定しました。
通常Bearerトークンが設定されるヘッダーです。
指定したヘッダーが設定されているかAPI Gatewayが検証し、設定されていなければ401 Unauthorized
となり、オーソライザー関数は実行されません。
ヘッダーが設定されていれば、Lambdaオーソライザーとして設定した関数が実行されます。
関数を実行する際にヘッダーの値は、authorizationToken
へマッピングされるので、Lambda関数からアクセスすることが可能です。
以下はLambda関数に渡されたevent値です。
{ "type": "TOKEN", "methodArn": "arn:aws:execute-api:ap-northeast-1:123456789012:91bnseszcb/iwasa-stage/POST/hoge-token", "authorizationToken": "hogeauth" }
なお、トークンのソースには任意のヘッダー名を指定することが可能です。
{ "type": "TOKEN", "methodArn": "arn:aws:execute-api:ap-northeast-1:123456789012:91bnseszcb/iwasa-stage/POST/hoge-token", "authorizationToken": "piyopiyo" }
指定したヘッダーがなければ同様に401となります。
トークンの検証
トークンベースオーソライザーの場合はトークンの検証機能を利用することが出来ます。
トークンの検証では正規表現を設定することで、Lambda関数実行前に簡単な入力チェックを行うことが可能です。
Bearerで始まる8文字の英字トークンを前提にした場合、hogeauth
だけだと、401 Unauthorized
になりました。
次に、Authorization: Bearer hogeauth
で送信してみましょう。
200 OK
になりました。
このように、無効なトークンでLambdaが実行されなくなるので、無駄な関数実行コストを抑制することが出来ます。
なお、あくまで入力チェックのような動きをするためのものなので、Lambda側でBearer
が除去されるなどの動きはしないので個別に実装は必要です。ご注意ください。
{ "type": "TOKEN", "methodArn": "arn:aws:execute-api:ap-northeast-1:123456789012:91bnseszcb/iwasa-stage/POST/hoge-token", "authorizationToken": "Bearer hogeauth" }
リクエストパラメータベース
続いて、リクエストパラメータベースのオーソライザーの挙動を確認します。
以下はオーソライザー未設定状態の実行結果です。
ここにリクエストパラメータベースオーソライザーを設定し、先程と同様に挙動を確認していきたいと思います。
オーソライザーの設定
先程はトークンのソースを設定しましたが、こちらで設定可能な項目は「IDソース」です。
また、IDソースはヘッダー以外の項目を指定でき、複数組み合わせて使用することも可能です。
まずは先程と同じように、AuthorizationヘッダーをIDソースに指定した場合を確認してみたいと思います。
こちらはリクエストにAuthorizationヘッダーを含まない場合です。
401 Unauthorized
になりました。
こちらはヘッダーを含む場合です。
200 OK
になりました。
IDパラメータに指定した項目がリクエストに含まれていない場合は、401となりオーソライザー関数は実行されないようです。
トークンベースと同じ動きですね。
独自のヘッダー(iwasaheader)をIDソースに指定してみましょう。
独自ヘッダーも問題ないですね。
次に、オーソライザー関数のeventを見てみましょう。
{ "type": "REQUEST", "methodArn": "arn:aws:execute-api:ap-northeast-1:123456789012:91bnseszcb/iwasa-stage/POST/hoge-request", "resource": "/hoge-request", "path": "/hoge-request", "httpMethod": "POST", "headers": { "Accept": "*/*", "Accept-Encoding": "gzip, deflate, br", "Content-Length": "48", "Content-Type": "application/json", "Host": "91bnseszcb.execute-api.ap-northeast-1.amazonaws.com", "iwasaheader": "fugafuga", "Postman-Token": "9f204d07-d43d-4296-8f40-93c969606c48", "User-Agent": "PostmanRuntime/7.26.8", "X-Amzn-Trace-Id": "Root=1-615e1cc5-16e542fa6fd92f470a8f431d", "X-Forwarded-For": "111.222.333.444", "X-Forwarded-Port": "443", "X-Forwarded-Proto": "https" }, "multiValueHeaders": { "Accept": [ "*/*" ], "Accept-Encoding": [ "gzip, deflate, br" ], "Content-Length": [ "48" ], "Content-Type": [ "application/json" ], "Host": [ "91bnseszcb.execute-api.ap-northeast-1.amazonaws.com" ], "iwasaheader": [ "fugafuga" ], "Postman-Token": [ "9f204d07-d43d-4296-8f40-93c969606c48" ], "User-Agent": [ "PostmanRuntime/7.26.8" ], "X-Amzn-Trace-Id": [ "Root=1-615e1cc5-16e542fa6fd92f470a8f431d" ], "X-Forwarded-For": [ "111.222.333.444" ], "X-Forwarded-Port": [ "443" ], "X-Forwarded-Proto": [ "https" ] }, "queryStringParameters": {}, "multiValueQueryStringParameters": {}, "pathParameters": {}, "stageVariables": {}, "requestContext": { "resourceId": "95hwbk", "resourcePath": "/hoge-request", "httpMethod": "POST", "extendedRequestId": "Gzlu5F5nNjMF1bA=", "requestTime": "06/Oct/2021:22:01:41 +0000", "path": "/iwasa-stage/hoge-request", "accountId": "123456789012", "protocol": "HTTP/1.1", "stage": "iwasa-stage", "domainPrefix": "91bnseszcb", "requestTimeEpoch": 1633557701517, "requestId": "94359609-a113-40bb-9ae7-6d292dfedcbc", "identity": { "cognitoIdentityPoolId": null, "accountId": null, "cognitoIdentityId": null, "caller": null, "sourceIp": "111.222.333.444", "principalOrgId": null, "accessKey": null, "cognitoAuthenticationType": null, "cognitoAuthenticationProvider": null, "userArn": null, "userAgent": "PostmanRuntime/7.26.8", "user": null }, "domainName": "91bnseszcb.execute-api.ap-northeast-1.amazonaws.com", "apiId": "91bnseszcb" } }
かなり情報量が多いですね。
リクエストパラメータベースの場合はこのようにリクエスト内の大部分の情報がオーソライザー関数に渡ってきます。
それらの任意の情報を使って、認証・認可処理を行うことが可能です。
ただし、bodyについては含まれていません。
ちなみに、IDソースとして指定した情報はこのオブジェクトには何もマッピング等はされません。ただし、情報が関数に渡ってこなくなるわけでもありません。
IDソースを指定しないでオーソライザーを使うことも可能です。
実は、IDソースはキャッシュキーとして使われます。
ですので、キャッシュを使わない場合は指定しなくても良いのです。
逆に、キャッシュを有効化した場合はIDソースの指定が必須となります。
とはいえ、キャッシュが無効化されていてもIDソースで指定した項目によってAPI Gatewayがリクエストに対する指定チェックをすることで無効な形式でのLambda実行の抑制には使えるので指定はしておいても良いかと思います。
複数のIDソースを指定
ちなみに、IDソースは複数指定することが可能です。
その場合、全てが揃っていないと401になります。
ヘッダー以外での検証が必要、あるいはヘッダーを含めて複数の値を検証で使用する必要がある、そういった場合にリクエストパラメータベースが使えそうです。
まとめ
- 1ヘッダーのトークン検証のみで良いのであれば、トークンベースが使えるか検討する
- インターフェースがシンプルになる。
- 正規表現を使って無効なトークンでLambdaが処理されるケースを軽減出来る。
- ヘッダー以外の項目、あるいは複数の値を検証に使う場合は、リクエストパラメータベースを検討する
- 色々渡されるので、色々出来る。